feat: ESI rate-limit support + typed SDK with OAS3 generator#22
Merged
Conversation
* update batches * update workflow for branch renaming * improve coverage
* Refactor RotatingFileLoggerTest.php for log level functionality. * lint
* fix: upgrade firebase/php-jwt from v5 to v6 - Bump composer requirement from ^5.4 to ^6.0 - Remove $allowed_algs parameter from JwtService::decodeJWT() — v6 embeds the algorithm inside Key objects; JWT::decode() no longer accepts a third argument - Update VerifyAccessToken to drop the ['RS256'] third argument - Update JwtServiceTest to use new Key($secret, 'HS256') instead of plain string keys - Update VerifyAccessTokenTest mock expectations to match new 2-arg decodeJWT() signature Resolves security advisories PKSA-y2cr-5h3j-g3ys and PKSA-2kqm-ps5x-s4f5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: update deprecated actions (checkout v4, codeclimate v9) - actions/checkout@v2 -> @v4 - paambaati/codeclimate-action@v2.6.0 -> @v9.0.0 Old versions crash with Node.js 20+ runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: replace broken codeclimate-action with direct pest run codeclimate.com/downloads/test-reporter returns 404 — the reporter binary has been removed upstream. Drop paambaati/codeclimate-action and run tests directly via pest --coverage --ci --min=100. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v6.x is also affected by PKSA-y2cr-5h3j-g3ys, so bump to ^7.0 which is unaffected. v7 enforces minimum key lengths (HS256: 32 bytes, RSA: 2048 bits) — update tests accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Magic property access bridge dropped for v4. All eveapi callers now use ->data->property directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace outdated echo $character_info usage example with proper EsiResponse consumption pattern - Document that response shapes are the consumer's responsibility (transport-only contract) - Show eveapi DTO pattern: XxxResponse::from($response->data) - Document ESI 1800-token/15-min rate limit and that throttling is handled by eveapi Horizon middleware Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… scaffold) - EsiResult<T> readonly class wraps typed data + pages + isCachedLoad - EsiClient::withToken(string): static for fluent authenticated calls - 30 resource factory methods on EsiClient (one per ESI tag group) - AbstractResource base class + 30 generated stub classes - EsiClient::$authentication changed from readonly to allow clone+reassign Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er comment
- buildInvokeCall: pass body params (in:body) as 6th arg to invoke()
- buildMethodSignature: resolve type from schema.type for body params, use mixed type
- generateResourceFile: fix {ESI_COMPATIBILITY_DATE} constant not interpolated in heredoc — pre-assign to $compatDate variable
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EsiResultTest: constructor defaults, explicit pages/cached, fromResponse with object body, X-Pages extraction, X-Kevinrob-Cache HIT/MISS - GeneratedResourcesTest: withToken() immutability, resource factory methods (characters/alliance/universe), typed DTO assertion on character endpoint, paginated asset endpoint with page count, cached-load propagation via HIT header - Helper makeAuthedClient() mocks CheckAccess::can() to avoid JWT decode on authenticated endpoints in unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- bin/generate.php now fetches from esi.evetech.net/meta/openapi.yaml
- Uses symfony/yaml for parsing (avoid native yaml_parse dependency)
- DTO class names now match schema names directly (e.g. CharactersDetail)
- Flat namespace: Responses/{SchemaName}.php (no tag subdirs)
- Typed method params: CharacterID x-common-model resolves to int
- Fixed: nullable mixed becomes mixed (PHP 8.4+ compliance)
- Fixed: array_map with cast for array<primitive> return types
- Added resources for 3 new tag groups: CorporationProjects, FreelanceJobs, Meta
- EsiClient: added corporationProjects(), freelanceJobs(), meta() methods
- Updated tests to use new DTO class names
PHPStan: 0 errors. All 87 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rator The Clones/ subdirectory was missed in the cleanup when switching to the OAS3 flat-namespace generator. The replacement file CharactersCharacterIdClonesGet.php already exists at the correct path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CCP has been observed changing ESI response fields without bumping the compatibility date (e.g. active_skill_level on 2026-02-27). Previously, required fields in from() did $data->field directly — a TypeError if ESI silently drops the field. Now all required primitive fields use a type-safe cast + zero fallback: (int)($data->field ?? 0) (string)($data->field ?? '') (bool)($data->field ?? false) (float)($data->field ?? 0.0) (array)($data->field ?? []) Required object (DTO) fields fall back to an empty stdClass so ::from() still runs rather than crashing on property access. Optional fields already used ?? and are unchanged. PHPStan: 0 errors. All 87 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… endpoints - Require seatplus/esi-schema:1.x-dev (VCS from github.com/seatplus/esi-schema) - Delete src/Generated/Responses/ — all DTOs now come from seatplus/esi-schema - Generator: no longer generates DTO files, only Resources - Generator: all DTO imports changed to Seatplus\EsiSchema\Responses\* - Object endpoints (single-object responses) now return the DTO directly instead of wrapping in EsiResult<T>. The DTO extends AbstractEsiDto so $dto->isCachedLoad and $dto->pages are available on the result. - Paginated array endpoints keep EsiResult<array<T>> (pages metadata required) - Update tests to reflect new return types - Update README: compatibility date notice + new SDK usage examples - ESI compatibility date: 2025-12-16 and forward Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove tools/swagger_download.php, tools/get_endpoints_and_scopes.php, tools/esi.json — all artefacts of the old Swagger 2.0 workflow; the generator now fetches OAS3 YAML directly from ESI - rector.php: PHP_83 → PHP_85 (SetList::PHP_85 confirmed present) - rector.php: replace tools path with bin (tools deleted, bin has generator) - Rector dry-run: 0 changes — codebase already fully PHP 8.5 compatible Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EsiClient now implements Seatplus\EsiSchema\Contracts\EsiTransportInterface - invoke() returns EsiRawResponse (wrapping GuzzleFetcher EsiResponse) converting: data, isCachedLoad(), pages → EsiRawResponse properties - All resource factory methods updated to return Seatplus\EsiSchema\Resources\* - Deleted src/Generated/Resources/ (34 files) — now live in esi-schema - Deleted bin/generate.php — single generator now in esi-schema - Updated composer.json: esi-schema dev-fix/ci-phpunit-config (pending PR merge) - Updated tests: EsiClientTest expects EsiRawResponse from invoke() - Updated tests: GeneratedResourcesTest uses esi-schema Resources + EsiResult - All 89 tests pass, PHPStan clean, 100% type coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligned with EsiTransportInterface change: version parameter removed since compatibility_date header handles versioning now. buildDataUri() hardcodes 'latest' in the URL path. Updated EsiClientTest to reflect /latest/ in expected URI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resources now use Seatplus\EsiSchema\EsiResult::fromRaw() — the EsiClient\EsiResult (which used fromResponse(EsiResponse)) is no longer referenced by anything. Removing it eliminates the confusion of two nearly-identical EsiResult classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…voke() - Lower PHP requirement to ^8.3 (no PHP 8.5 features used yet) - EsiClient::invoke() now propagates rateLimitRemaining, rateLimitUsed, retryAfter from EsiResponse headers into EsiRawResponse - Detects cursor token in response body and populates EsiRawResponse::cursor (for x-pagination: cursor routes like freelance-jobs, projects) - 3 new tests covering rate-limit propagation and cursor extraction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PHP 8.5 released and available. No code changes required — the package already uses PHP 8.3+ features only (typed class constants). - composer.json: PHP ^8.3 → ^8.5 - composer.json: nesbot/carbon ^2.53 → ^3.0 (required for Laravel 13 compat) - tests/Unit/EsiClientTest.php: pint binary_operator_spaces fix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Switch from dev-feat/typed-operation-meta branch alias to stable ^1.0 - Remove VCS repository override (package is on Packagist) - Remove minimum-stability: dev and prefer-stable (no longer needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e in EsiResponse - Add data-provider test covering all 30 remaining factory methods on EsiClient (calendar, clones, contacts, … meta) to reach 100% line coverage - Remove unreachable $window === null guard in parseRatelimitWindowSeconds(): the str_contains() check above guarantees explode() always yields index [1], making the null branch dead code and untestable Fixes CI code-coverage failure (was 90.5 %, now 100.0 %) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The source only uses PHP 8.1 features (readonly properties, new-in-initializers). All runtime deps (Carbon ^3.0, monolog ^3.7, guzzle-cache-middleware ^4.0) support PHP 8.1+. PHP 8.1 is EOL, so - require.php: ^8.5 → ^8.2 - remove unused symfony/yaml dev dependency (leftover from esi-schema codegen work; never referenced in esi-client source or tests) - ci: php-version 8.5 → 8.2 in both tests.yml and formats.yml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Packagist derives the version from git tags. A hardcoded "version" field overrides tag detection and would pin the package permanently at 4.0.0 regardless of future tags. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pestphp/pest ^4.0 and seatplus/esi-schema 1.0.0 both require php ^8.3, making 8.2 uninstallable. ^8.3 is the correct minimum — still well below the previous ^8.5. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rdcoded scope map (#25) * refactor: replace CheckAccess with assertScope() from EsiTransportInterface Scope enforcement now lives where scope is defined. Each generated execute() calls $transport->assertScope(self::REQUIRED_SCOPE) before any HTTP call. The transport (EsiClient) implements the check against the JWT token. Changes: - EsiClient: implement assertScope(?string $scope): void - null = public endpoint → no-op - non-null = must be present in JWT scopes → ScopeAccessDeniedException - EsiClient: remove CheckAccess wiring from constructor and withToken() - EsiClient: remove hasAccess() pre-flight from invoke() - EsiScopeAccessDeniedException: now extends ScopeAccessDeniedException from esi-schema for backward compatibility - Delete CheckAccess.php (250-line hardcoded scope map — no longer needed) - Delete CheckAccessTest.php (replaced by assertScope tests in EsiClientTest) - Bump seatplus/esi-schema: ^1.1 (requires assertScope in EsiTransportInterface) - Update GeneratedResourcesTest: use real JWT tokens, remove CheckAccess mock - Update EsiClientTest: replace access-denied test with assertScope tests Requires seatplus/esi-schema ^1.1 (PR #4). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: use dev-feat/assert-scope alias until esi-schema 1.1.0 is tagged Until esi-schema PR #4 is merged and 1.1.0 tagged on Packagist, use the branch dev alias so CI can resolve the dependency. Will be reverted to ^1.1 once tag exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: trigger CI — add assertScope docblock, watch composer.json in formats workflow - Add docblock to EsiClient::assertScope() explaining null semantics - Update formats.yml to also trigger on composer.json changes and pull_request targeting feat/esi-rate-limit-overhaul Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove minimum-stability=dev — explicit dev-branch constraint is sufficient Explicit dev-* constraints override minimum-stability per-package, so setting minimum-stability=dev globally was unnecessary and caused pest-plugin-type-coverage to resolve a dev build instead of stable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: use stable seatplus/esi-schema ^1.1 (1.1.0 now tagged on Packagist) Replaces the temporary dev-feat/assert-scope alias now that esi-schema PR #4 has been merged and tagged 1.1.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove feature branch name from formats.yml pull_request trigger Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: drop EsiScopeAccessDeniedException — use ScopeAccessDeniedException from esi-schema directly No backwards compatibility needed. Callers should catch Seatplus\EsiSchema\Contracts\ScopeAccessDeniedException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… spec The ESI OpenAPI 3.1 spec (esi.evetech.net/meta/openapi.yaml) defines: servers: [url: https://esi.evetech.net] paths: /alliances, /characters/{character_id}/assets, … No version prefix anywhere. Versioning is handled by X-Compatibility-Date header (already sent by GuzzleFetcher). The /latest/ prefix was a Swagger 2.0 basePath artifact. Changes: - buildDataUri(): /{path}/ instead of /latest/{path}/ - EsiConfiguration: default compatibility_date to '2025-12-16' (matches esi-schema generation date; update when regenerating) - Update tests accordingly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Strings: replace sprintf() and . concatenation with string interpolation throughout EsiRateLimitedException, EsiErrorLimitedException, EsiClient, GuzzleFetcher, UpdateRefreshTokenService, EsiResponse, RotatingFileLogger - Fix latent bug: logFetcherActivity used %F with number_format result, double-formatting the float (0.12 → 0.120000); interpolation uses the number_format string directly - Docblocks: remove duplicate docblock on EsiClient::assertScope(); remove useless 'FileLogger constructor.' description from RotatingFileLogger - Comments: remove inline comments that restate the code they precede - Naming: rename private snake_case members in EsiResponse to camelCase (get_data→getData, error_message→errorMessage, expires_at→expiresAt, cache_loaded→cacheLoaded) - Constructor: expand UpdateRefreshTokenService single-line constructor to multi-line promoted-parameter style (one param per line, trailing comma) All 113 tests pass, Pint clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR completes the transport inversion architecture for the ESI SDK:
Changes
EsiClient implements EsiTransportInterfaceinvoke()now returnsEsiRawResponse(from esi-schema) instead of the internalEsiResponsedata,isCachedLoad,pages→EsiRawResponsepropertiesalliance(),characters(), ...) now returnSeatplus\EsiSchema\Resources\*Deleted (moved to esi-schema)
src/Generated/Resources/— 34 files deletedbin/generate.php— single generator now lives in esi-schemaDependency
EsiTransportInterface,EsiRawResponse,AbstractResource, Resources)Architecture
Usage
Merge order
Tests